#pragma once

/// @file userver/tracing/span.hpp
/// @brief @copybrief tracing::Span

#include <optional>
#include <string_view>

#include <userver/logging/log.hpp>
#include <userver/logging/log_extra.hpp>
#include <userver/tracing/scope_time.hpp>
#include <userver/tracing/tracer_fwd.hpp>
#include <userver/utils/impl/internal_tag.hpp>
#include <userver/utils/impl/source_location.hpp>

USERVER_NAMESPACE_BEGIN

namespace engine {
class TracePlugin;
}

namespace tracing {

class SpanBuilder;
struct SpanEvent;

/// @brief Measures the execution time of the current code block, links it with
/// the parent tracing::Spans and stores that info in the log.
///
/// Logging of spans can be controlled at runtime via @ref USERVER_NO_LOG_SPANS, provided that you
/// `Append` @ref components::LoggingConfigurator component or use @ref components::CommonComponentList.
///
/// See @ref scripts/docs/en/userver/logging.md for usage examples and more
/// descriptions.
///
/// @warning Shall be created only as a local variable. Do not use it as a
/// class member!
class Span final {
public:
    class Impl;

    /// Use implicit task-local storage for parent identification, takes TraceID and Link from the parent `Span`.
    ///
    /// For extremely rare cases where a new Trace ID is required, use @ref tracing::Span::MakeSpan.
    explicit Span(
        std::string name,
        const utils::impl::SourceLocation& source_location = utils::impl::SourceLocation::Current()
    );

    /// Sets an arbitrary `Span` as parent. If `parent == nullptr`, then creates a root `Span`.
    ///
    /// Useful in extremely rare cases. Prefer the constructor above by default.
    explicit Span(
        std::string name,
        const Span* parent,
        ReferenceType reference_type = ReferenceType::kChild,
        const utils::impl::SourceLocation& source_location = utils::impl::SourceLocation::Current()
    );

    Span(Span&& other) noexcept;
    Span(const Span& other) = delete;
    Span& operator=(Span&&) = delete;
    Span& operator=(const Span&) = delete;
    ~Span();

    /// @brief Returns the Span of the current task.
    ///
    /// Should not be called in non-coroutine
    /// context. Should not be called from a task with no alive Span.
    ///
    /// Rule of thumb: it is safe to call it from a task created by
    /// utils::Async/utils::CriticalAsync/utils::PeriodicTask. If current task was
    /// created with an explicit engine::impl::*Async(), you have to create a Span
    /// beforehand.
    static Span& CurrentSpan();

    /// @brief Returns nullptr if called in non-coroutine context or from a task
    /// with no alive Span; otherwise returns the Span of the current task.
    static Span* CurrentSpanUnchecked();

    /// Factory function for extremely rare cases of creating a Span with custom
    /// IDs; prefer Span constructor instead.
    ///
    /// @return A new Span with the specified IDs.
    /// @param name Name of a new Span.
    /// @param trace_id New Trace ID; if empty then the Trace ID is autogenerated.
    /// @param parent_span_id ID of the parent Span, can be empty.
    /// @param source_location Implementation detail, do not touch.
    static Span MakeSpan(
        std::string name,
        std::string_view trace_id,
        std::string_view parent_span_id,
        const utils::impl::SourceLocation& source_location = utils::impl::SourceLocation::Current()
    );

    /// Factory function for extremely rare cases of creating a Span with custom
    /// IDs; prefer Span constructor instead.
    ///
    /// @return A new Span with the specified IDs.
    /// @param name Name of a new Span.
    /// @param trace_id New Trace ID; if empty then the Trace ID is autogenerated.
    /// @param parent_span_id ID of the parent Span, can be empty.
    /// @param link The new Link; if empty the Link is autogenerated.
    /// @param source_location Implementation detail, do not touch.
    static Span MakeSpan(
        std::string name,
        std::string_view trace_id,
        std::string_view parent_span_id,
        std::string_view link,
        const utils::impl::SourceLocation& source_location = utils::impl::SourceLocation::Current()
    );

    /// Factory function for rare cases of creating a root Span that starts
    /// the trace_id chain, ignoring `CurrentSpan`, if any. Useful
    /// in background jobs, periodics, distlock tasks, cron tasks, etc.
    /// The result of such jobs is not directly requested by anything.
    ///
    /// @return A new Span that is the root of a new Span hierarchy.
    /// @param name Name of a new Span
    /// @param source_location Implementation detail, do not touch.
    static Span MakeRootSpan(
        std::string name,
        const utils::impl::SourceLocation& source_location = utils::impl::SourceLocation::Current()
    );

    /// Create a child which can be used independently of the parent.
    ///
    /// The child shares no state with its parent. If you need to run code in
    /// parallel, create a child span and use the child in a separate task.
    Span CreateChild(
        std::string name,
        const utils::impl::SourceLocation& source_location = utils::impl::SourceLocation::Current()
    ) const;

    Span CreateFollower(
        std::string name,
        const utils::impl::SourceLocation& source_location = utils::impl::SourceLocation::Current()
    ) const;

    /// @brief Creates a tracing::ScopeTime attached to the span.
    ScopeTime CreateScopeTime();

    /// @brief Creates a tracing::ScopeTime attached to the Span and starts
    /// measuring execution time.
    /// Tag `{scope_name}_time` with elapsed time is added to result span.
    ///
    /// @note `name` parameter is expected to satisfy snake case.
    /// Otherwise, it is converted to snake case.
    ScopeTime CreateScopeTime(std::string name);

    /// Returns total time elapsed for a certain scope of this span.
    /// If there is no record for the scope, returns 0.
    ScopeTime::Duration GetTotalDuration(const std::string& scope_name) const;

    /// Returns total time elapsed for a certain scope of this span.
    /// If there is no record for the scope, returns 0.
    ///
    /// Prefer using Span::GetTotalDuration()
    ScopeTime::DurationMillis GetTotalElapsedTime(const std::string& scope_name) const;

    /// Add a tag that is used on each logging in this Span and all
    /// future children.
    void AddTag(std::string key, logging::LogExtra::Value value);

    /// Add a tag that is used on each logging in this Span and all
    /// future children. It will not be possible to change its value.
    void AddTagFrozen(std::string key, logging::LogExtra::Value value);

    /// Add a tag that is local to the Span (IOW, it is not propagated to
    /// future children) and logged only once in the destructor of the Span.
    void AddNonInheritableTag(std::string key, logging::LogExtra::Value value);

    /// @overload AddNonInheritableTag
    void AddNonInheritableTags(const logging::LogExtra&);

    /// Add an event to Span.
    void AddEvent(SpanEvent&& event);

    /// Add an event (without attributes) to Span.
    /// @overload AddEvent
    void AddEvent(std::string_view event_name);

    /// @brief Sets log level with which the current span itself is written into the tracing
    /// system.
    ///
    /// This (mostly) does not affect logs written within the span,
    /// unlike @ref tracing::Span::SetLocalLogLevel.
    ///
    /// If `Span`'s log level is less than the global logger's log level, then the span is
    /// not written out. In that case:
    /// * nested logs are still written to the logging system;
    /// * they inherit `trace_id`, `link` and `span_id` of the nearest *written* `Span` object;
    /// * tags are still inherited from the *nearest* `Span` even if it is hidden;
    ///    * this allows to use `Span`s as tag scopes regardless of their tracing purposes.
    ///
    /// Tracing systems use span's log level to highlight `warning` and `error` spans.
    void SetLogLevel(logging::Level log_level);

    /// See @ref tracing::Span::SetLogLevel.
    logging::Level GetLogLevel() const;

    /// @brief Sets an additional cutoff for the logs written in the scope of this `Span`,
    /// and in nested scopes recursively.
    ///
    /// For example, if the global log level is `info`, and the current `Span` has
    /// (own or inherited) local log level `warning`, then all `LOG_INFO`s within the current
    /// scope will be thrown away.
    ///
    /// The cutoff also applies to the span itself. If the @ref tracing::Span::SetLogLevel "span's log level"
    /// is less than the local log level, then the span is not written to the tracing system.
    ///
    /// Local log level of child spans can override local log level of parent spans in both directions.
    /// For example:
    /// * if local log level of a parent `Span` is `warning`,
    /// * and local log level of a child span is set `info`,
    /// * then `info` logs within that `Span` will be written,
    /// * as long as the global log level is not higher than `info`.
    ///
    /// Currently, local log level cannot override the global log level of the logger.
    /// For example, if the global log level is `info`, and the current `Span` has
    /// (own or inherited) local log level `debug`, then all `LOG_DEBUG`s within the current
    /// scope will **still** be thrown away.
    void SetLocalLogLevel(std::optional<logging::Level> log_level);

    /// See @ref tracing::Span::SetLocalLogLevel.
    std::optional<logging::Level> GetLocalLogLevel() const;

    /// Set link - a request ID within a service. Can be called only once.
    ///
    /// Propagates within a single service, but not from client to server. A new
    /// link is generated for the "root" request handling task.
    void SetLink(std::string_view link);

    /// Set parent link - request ID of the upstream service. Can only be called once.
    ///
    /// Propagates within a single service.
    void SetParentLink(std::string_view parent_link);

    /// Get link - a request ID within the service.
    ///
    /// Propagates within a single service, but not from client to server. A new
    /// link is generated for the "root" request handling task.
    std::string_view GetLink() const;

    /// Set parent link - request ID of the upstream service. Can only be called once.
    ///
    /// Propagates within a single service.
    std::string_view GetParentLink() const;

    /// An ID of the request that does not change from service to service.
    ///
    /// Propagates both to sub-spans within a single service, and from client
    /// to server
    std::string_view GetTraceId() const;

    /// Identifies a specific span. It does not propagate.
    std::string_view GetSpanId() const;

    /// Span ID of the nearest loggable parent span, or empty string if none exists.
    std::string_view GetParentId() const;

    /// Span ID of the nearest loggable span within the span chain, including the current span.
    /// If the current span and all parent spans will not be logged, returns `std::nullopt`.
    std::optional<std::string_view> GetSpanIdForChildLogs() const;

    /// Get name the Span was created with
    std::string_view GetName() const;

    /// @returns true if this span would be logged with the current local and
    /// global log levels to the default logger.
    bool ShouldLogDefault() const noexcept;

    /// Detach the Span from current engine::Task so it is not
    /// returned by CurrentSpan() any more.
    void DetachFromCoroStack();

    /// Attach the Span to current engine::Task so it is returned
    /// by CurrentSpan().
    void AttachToCoroStack();

    std::chrono::system_clock::time_point GetStartSystemTime() const;

    /// @cond
    // For internal use only.
    void AddTags(const logging::LogExtra&, utils::impl::InternalTag);

    // For internal use only.
    impl::TimeStorage& GetTimeStorage(utils::impl::InternalTag);

    // For internal use only.
    void LogTo(utils::impl::InternalTag, logging::impl::TagWriter writer) const;
    /// @endcond

private:
    struct OptionalDeleter {
        void operator()(Impl*) const noexcept;

        static OptionalDeleter ShouldDelete() noexcept;

        static OptionalDeleter DoNotDelete() noexcept;

    private:
        explicit OptionalDeleter(bool do_delete) : do_delete_(do_delete) {}

        const bool do_delete_;
    };

    friend class SpanBuilder;
    friend class TagScope;
    friend class InPlaceSpan;
    friend class engine::TracePlugin;

    explicit Span(std::unique_ptr<Impl, OptionalDeleter>&& pimpl);

    std::string_view GetTag(std::string_view tag) const;

    std::unique_ptr<Impl, OptionalDeleter> pimpl_;
};

namespace impl {

class DetachLocalSpansScope final {
public:
    DetachLocalSpansScope() noexcept;
    ~DetachLocalSpansScope();

    DetachLocalSpansScope(DetachLocalSpansScope&&) = delete;
    DetachLocalSpansScope& operator=(DetachLocalSpansScope&&) = delete;

private:
    struct Impl;
    utils::FastPimpl<Impl, 16, 8> impl_;
};

struct LogSpanAsLastNoCurrent final {
    const Span& span;
};

logging::LogHelper& operator<<(logging::LogHelper& lh, LogSpanAsLastNoCurrent span);

}  // namespace impl

}  // namespace tracing

USERVER_NAMESPACE_END
